/*
 * Copyright (c) 2003, 2004 Ulrich Hilger, http://dev.uhilger.de, all rights reserved.
 *
 * Published under the terms and conditions of the Apache License 2.0,
 * see http://www.apache.org/licenses/LICENSE-2.0
 */

package de.uhilger.app;

import java.util.logging.Level;
import java.io.OutputStream;
import java.util.logging.Logger;
import java.util.logging.Handler;
import java.util.logging.StreamHandler;
import javax.help.HelpBroker;
import java.net.URL;
import javax.help.HelpSet;
import javax.help.HelpSetException;
import java.util.ResourceBundle;
import java.util.Locale;
import java.io.File;
import java.util.logging.FileHandler;
import java.util.logging.LogRecord;
import de.uhilger.app.ui.MainFrame;
import de.uhilger.app.ui.DefaultAboutDialog;
import java.util.Hashtable;
import java.util.MissingResourceException;
import javax.swing.Icon;
import javax.swing.Action;
import javax.swing.ImageIcon;

/**
 * Base class for applications
 *
 * <p>An application typically has some basic functionality such as logging and
 * online help. This abstract base class implements such basic functionality so
 * that application objects can extend from it.</p>
 *
 * <p>In particular, it implements
 * <ul>
 * <li><p>a Logger for log messages and global error handling along
 * with some common logging methods</p></li>
 * <li><p>a help broker for a JavaHelp help set</p></li>
 * <li><p>a ResourceMenu for dynamic component creation from a description resource
 * and for caching of action objects</p></li>
 * <li><p>a ResourceBundle for internationalized UI components</p></li>
 * </ul>
 * </p>
 *
 * <p>See the various methods for comments about how the implementation works
 * by default</p>
 *
 * <p>To create a new application based on the Application package</p>
 * <ol><li><p>create a new java source code file "TestApp.java" with the following content,
 * place it into a new directory in the file system and compile it:
 *
 * <pre>
 * package com.lightdev.app.test;
 *
 * import com.lightdev.app.DefaultApplication;
 * import javax.swing.UIManager;
 *
 * public class TestApp extends DefaultApplication {
 *   public TestApp() {
 *     super();
 *   }
 *   public static void main(String[] args) {
 *     try {
 *       UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
 *     }
 *     catch(Exception e) {
 *       e.printStackTrace();
 *     }
 *     new TestApp();
 *   }
 * }
 * </pre></p></li>
 * <li><p>make a copy of file "DefaultApplication.properties" from the application
 * package, rename it to "TestApp.properties" and place it into the folder where the file "TestApp.class"
 * resides in.</p></li></ol>
 *
 * <p>That's it, you are ready to build your new application from the newly created directory and files.</p>
 *
 * <p><b>Please note:</b> You need Application.jar and jhall.jar (the JavaHelp extension) in the classpath
 * of your new application so that all classes are present.</p>
 *
 * @author Ulrich Hilger, <a href="http://dev.uhilger.de" target="_blank">http://dev.uhilger.de</a>
 * @author Published under the terms and conditions of
 * the <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank">Apache License 2.0</a>
 *
 * @version 2, February 12, 2004
 */
public abstract class Application {

  /**
   * constructor
   *
   * <p>Looks for a resource bundle which is named similar to the class name inside the package
   * of this class</p>
   *
   * @param logStream OutputStream the stream to log to
   * @param level Level log level to recognize in logs
   * @param logDir String a directory to log to (e.g. ".appName"  + File.separator + "logs")
   */
  public Application(OutputStream logStream, Level level, String logDir) {
    resources = ResourceBundle.getBundle(getClass().getName(), Locale.getDefault());
    //menu = new ResourceMenu();
    setLogging(logStream, level, logDir);
    initHelp();
    initActions();
  }

  /**
   * constructor
   *
   * <p>Looks for a resource bundle which is named similar to the class name inside the package
   * of this class</p>
   *
   * @param level Level log level to recognize in logs
   * @param logDir String a directory to log to (e.g. ".appName"  + File.separator + "logs")
   */
  public Application(Level level, String logDir) {
    this(System.out, level, logDir);
  }

  /**
   * constructor with default parameters
   */
  public Application() {
    this(Level.FINEST, "");
  }

  /**
   * get the name of this application
   *
   * @return the name of this application
   */
  protected String getAppName() {
    return APP_NAME;
  }

  /**
   * set logging
   *
   * @param stream OutputStream a stream to log to
   * @param level Level the log level to use
   * @param dir String the directory to log to
   */
  protected void setLogging(OutputStream stream, Level level, String dir) {
    initLogger(stream, level, dir);
  }

  /**
   * set the log level
   * @param level Level  the level to log
   */
  protected void setLogLevel(Level level) {
    logger.setLevel(level);
  }

  /**
   * initialize the logger
   *
   * <p>This method creates two loggers, one for output to the console
   * and one for output to an XML file. XML file output is done to
   * a directory named [user home]/.[application class name]/logs</p>
   *
   * <p>A log file is written up to the size of 100.000 bytes, then another
   * log file is started. Once the second log file is full, the first gets
   * overwritten, and the process starts all over. When parallel processes
   * attempt to write to the same log file, a unique number is appended to
   * the log file name. I.e. log files are named log0.0.txt, log0.1.txt,
   * log0.2.txt, etc. by each activity that is logged in parallel to another
   * activity. A log file reader therefore needs to read from any of such log
   * files and merge log entries accordingly.</p>
   *
   * @param logDestination the OutputStream to log to
   * @param level the log level up to which logging is performed
   * @param dir String the directory to log to (e.g. ".appName"  + File.separator + "logs")
   */
  private void initLogger(OutputStream logDestination, Level level, String dir) {
    if (dir.length() < 1) {
      String cName = getClass().getName();
      dir = cName.substring(cName.lastIndexOf('.')).toLowerCase() + File.separator + "logs";
    }
    logger = Logger.getLogger(getClass().getName());
    Handler[] h = logger.getHandlers();
    logger.setUseParentHandlers(false);
    logger.setLevel(level);
    logger.setFilter(null);
    for (int i = 0; i < h.length; i++) {
      logger.removeHandler(h[i]);
    }
    try {
      // set our stream handler
      TextComponentLogFormatter formatter = new TextComponentLogFormatter();
      sh = new StreamHandler(logDestination, formatter/*new StreamLogFormatter()/*new SimpleFormatter()*/);
      sh.setLevel(level);
      //sh.setFormatter(new StreamLogFormatter());
      logger.addHandler(sh);
      // set our file handler
      String fName = System.getProperty("user.home") + File.separator + dir;
      File f = new File(fName);
      if (!f.exists()) {
        f.mkdirs();
      }
      fh = new FileHandler(fName + File.separator + "log%g.%u.txt", 100000, 2, true);
      fh.setFormatter(/*new XMLFormatter()*/formatter);
      logger.addHandler(fh);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  /**
   * log an information message
   *
   * @param msg the message string
   * @param source Class the class that logged this message
   * @param level Level the log level the message has
   */
  public static void log(String msg, Class source, Level level) {
    LogRecord r = new LogRecord(level, msg);
    r.setSourceClassName(source.getName());
    logger.log(r);
    Handler[] h = logger.getHandlers();
    for (int i = 0; i < h.length; i++) {
      h[i].flush();
    }
  }

  /**
   * Get the logger for this application
   * @return  the logger
   */
  public Logger getLogger() {
    return logger;
  }

  /**
   * log an exception by writing the stack trace
   * to the log as a warning
   *
   * @param ex the exception to log the stack trace for
   * @param source Class the class the error came from
   */
  public static void logStackTrace(Throwable ex, Class source) {
    StackTraceElement[] stackTrace = ex.getStackTrace();
    StringBuffer buf = new StringBuffer();
    String indent = "  ";
    String newLine = System.getProperty("line.separator"); //(String) java.security.AccessController.doPrivileged(
    //new sun.security.action.GetPropertyAction("line.separator"));
    buf.append(ex.toString() + ": " + ex.getLocalizedMessage());
    for (int i = 0; i < stackTrace.length; i++) {
      buf.append(newLine);
      buf.append(indent);
      buf.append(stackTrace[i].toString());
    }
    log(buf.toString(), source, Level.SEVERE);
  }

  /**
   * initialize the command table of the resource menu with
   * this application's actions (here to override by subclasses)
   */
  protected void initActions() {
  }

  /**
   * called by default by method exitApplication. Override this method to customize
   * exit hnadling, i.e. if a certain condition causes the application to temporarily
   * being unable to terminte, this method should return false
   *
   * @return boolean ture, if the application can terminate at the time this method is called
   */
  protected boolean canExit() {
    return true;
  }

  /**
   * Called by default by class AboutAppAction. Override this method
   * to implement custom about handling.
   */
  public void about() {
    MainFrame mf = DefaultApplication.findMainFrame();
    if (mf != null) {
      DefaultAboutDialog dlg =
              new DefaultAboutDialog(mf, getResourceString(Application.RB_KEY_ABOUT_APP_DIALOG_TITLE));
      dlg.setLocationRelativeTo(mf);
      dlg.setModal(true);
      dlg.setVisible(true);
    }
  }

  /**
   * get the HelpBroker of this application
   *
   * @return the HelpBroker
   */
  public static HelpBroker getHelpBroker() {
    return hb;
  }

  /**
   * Called by default by class ExitAction. Override this method
   * to implement custom exit handling.
   */
  public void exitApplication() {
    Application.log("exitApplication", getClass(), Level.FINEST);
    if (canExit()) {
      Application.log(
              getResourceString(Application.RB_KEY_LOG_APP_TERMINATED),
              getClass(), Level.INFO);
      System.exit(0);
    } else {
      Application.log(
              getResourceString(Application.RB_KEY_LOG_APP_CAN_NOT_EXIT),
              getClass(), Level.INFO);
    }
  }

  /**
   * initialize JavaHelp
   *
   * expects a help set named as defined in constant APP_HELP_NAME in a subpackage named
   * as defined in constant APP_HELP_NAME inside this package
   *
   * if this Application class is extended by class AllDay which resides
   * in package com.lightdev.app.allday for instance
   * and APP_HELP_NAME is defined as 'help', this method will look for
   * com.lightdev.app.allday.help.help.hs
   */
  private void initHelp() {
    try {
      if (hb == null) {
        URL url = this.getClass().getResource(APP_HELP_NAME +
                URL_SEPARATOR + APP_HELP_NAME +
                JAVA_HELP_EXT);
        hb = new HelpSet(null, url).createHelpBroker();
        hb.initPresentation();
      }
    } catch (HelpSetException ex) {
      logStackTrace(ex, getClass());
    }
  }

  /**
   * get the ResourceBundle of this application
   * @return the ResourceBundle
   */
  public ResourceBundle getBundle() {
    return resources;
  }

  /**
   * get an action from the commands table
   *
   * @param cmd  the name of the action the get
   * @return the action found for the given name
   */
  public Action getAction(String cmd) {
    return (Action) commands.get(cmd);
  }

  /**
   * get a string from the resources file
   *
   * @param resources  the ResourceBundle to get the string from
   * @param nm  the key of the string
   * @return the string for the given key or null if not found
   */
  public String getResourceString(String nm) {
    String str = null;
    try {
      str = resources.getString(nm);
    } catch (MissingResourceException mre) {
      //logStackTrace(mre, getClass());
    }
    return str;
  }

  /**
   * add an action to this <code>DynamicResource</code>
   *
   * @param uiClass a class residing in the package having access to ui resources
   * @param resources the ResourceBundle to get resources such as icons and strings from
   * @param cmd  the internal identifier for the action
   * @param action  the action to associate with cmd
   */
  public void addAction(Class uiClass, String cmd, Action action) {
    action.putValue(Action.NAME, cmd);
    Icon icon = getIconForCommand(resources, uiClass, cmd);
    if (icon != null) {
      action.putValue(Action.SMALL_ICON, icon);
    }
    String toolTip = getResourceString(cmd + toolTipSuffix);
    if (toolTip != null) {
      action.putValue(Action.SHORT_DESCRIPTION, toolTip);
    }
    commands.put(cmd, action);
  }

  /**
   * get an icon from the application's resource bundle
   *
   * <p>For some reason I am not able to get a resource when the class
   * from which getResource is called is in a different package, which
   * is why this method requires any class that resides in the package
   * having ui resoruces</p>
   *
   * @param uiClass Class a class that resides in the package having ui resources
   * @param cmd String the action command
   * @return Icon the icon or null
   */
  public Icon getIconForCommand(ResourceBundle resources, Class uiClass, String cmd) {
    return getIconForName(uiClass, cmd + imageSuffix);
  }

  /**
   * get an icon from the application's resource bundle
   *
   * <p>For some reason I am not able to get a resource when the class
   * from which getResource is called is in a different package, which
   * is why this method requires any class that resides in the package
   * having ui resoruces</p>
   *
   * @param uiClass Class a class that resides in the package having ui resources
   * @param cmd String the action command
   * @return Icon the icon or null
   */
  public Icon getIconForName(Class uiClass, String name) {
    Icon icon = null;
    URL url = getResource(uiClass, name);
    if (url != null) {
      icon = new ImageIcon(url);
    }
    return icon;
  }

  /**
   * get the location of a resource.
   *
   * @param c Class any ui class for finding the package
   * @param key  the key of the resource in the resource file
   * @return the resource location as a URL
   */
  public URL getResource(Class c, String key) {
    String name = getResourceString(key);
    if (name != null) {
      URL url = c.getResource(name);
      return url;
    }
    return null;
  }
  /**
   * get the ResourceMenu object of this application
   *
   * @return the ResourceMenu object
   */
  /*public static ResourceMenu getResMenu() {
  return menu;
  }*/

  /* ------------------- class fields start ------------------------ */
  /** central action table, maps action commands to actions */
  private Hashtable commands = new Hashtable();
  /** resource bundle */
  protected ResourceBundle resources;
  /** the ResourceMenu object of this application */
  //private static ResourceMenu menu;
  /** logger for this Application */
  protected static Logger logger;
  /** the handler to log to a stream */
  private static StreamHandler sh;
  /** the handler to log to a file */
  private static FileHandler fh;
  /** help broker for this Application */
  private static HelpBroker hb;
  /** string to display when a resource could not be loaded */
  public static final String RESOURCE_NOT_FOUND = "resource not found";
  /** the name of this application */
  private static final String APP_NAME = "Application";
  /** resource bundle key to identify the string to use to log that the application has started */
  public static final String RB_KEY_LOG_APP_STARTED = "logAppStarted";
  /** resource bundle key to identify the string to use to log that the application has ended */
  public static final String RB_KEY_LOG_APP_TERMINATED = "logAppTerminated";
  /** resource bundle key to identify the string to use to log that the application can not end */
  public static final String RB_KEY_LOG_APP_CAN_NOT_EXIT = "logAppCanNotExit";
  /** resource bundle key to identify the string to use for the title of this application's about dialog */
  public static final String RB_KEY_ABOUT_APP_DIALOG_TITLE = "aboutDialogTitle";
  /** name constant for labels in the resource file */
  public static final String labelSuffix = "Label";
  /** name constant for action commands in the resource file */
  public static final String actionSuffix = "Action";
  /** name constant for indicating image resources in the resource file */
  public static final String imageSuffix = "Image";
  /** name constant for tool tip strings in the resource file */
  public static final String toolTipSuffix = "Tip";
  /** name constant for selected icon names in the resource file */
  public static final String selectedIconSuffix = "SelectedIcon";

  /* constants for JavaHelp usage */
  public static final String APP_HELP_NAME = "help";
  public static final String JAVA_HELP_EXT = ".hs";
  public static final String URL_SEPARATOR = "/";

  /* ------------------- class fields end ------------------------ */
}
